<?php


namespace App\Utils;

use Closure;

class ExcelSheet
{
    private $rowCount = 0;
    private $columnCount = 0;
    private $data;
    private $horizontal = true;
    private $childrenField = 'children';
    private $insertHiddenRow;
    private $insertHiddenColumn;
    private $hiddenField = 'id';
    private $horizontalHiddenClosure;
    private $verticalHiddenClosure;
    private $valueField = 'name';
    private $valueClosure;
    private $startRow = 0;
    private $startColumn = 0;
    private $insertCellDataOnLeafCellClosure;
    private $insertExtraCellClosure;
    private $mergeCell;
    private $cellDataVFieldClosure;
    private $cellDataFieldBold = true;
    private $cellDataFieldTextBreak = 2;
    private $cellDataFieldHorizontalType = 0;
    private $cellDataFieldVerticalType = 0;
    private $cellDataFieldColor = '#000';
    private $customResponseDataClosure;

    /**
     * ExcelSheet constructor.
     * @param array $data
     * @param bool $vertical
     */
    public function __construct(array $data, bool $vertical = false)
    {
        $this->data = $data;
        $this->setVertical($vertical);
    }

    /**
     * @return array
     */
    public function getData(): array
    {
        return $this->data;
    }

    /**
     * @param string $field
     * @return $this
     */
    public function setChildrenField(string $field): ExcelSheet
    {
        $this->childrenField = $field;

        return $this;
    }

    /**
     * @param bool $vertical
     * @return $this
     */
    public function setVertical(bool $vertical = true): ExcelSheet
    {
        $this->horizontal = !$vertical;

        return $this;
    }

    /**
     * @param string $field
     * @return $this
     */
    public function setHiddenField(string $field): ExcelSheet
    {
        $this->hiddenField = $field;

        return $this;
    }

    /**
     * @param string $field
     * @return $this
     */
    public function setValueField(string $field): ExcelSheet
    {
        $this->valueField = $field;

        return $this;
    }

    /**
     * @param Closure $closure
     * @return $this
     */
    public function setValueClosure(Closure $closure): ExcelSheet
    {
        $this->valueClosure = $closure;

        return $this;
    }

    /**
     * @param string $color
     * @return $this
     */
    public function setCellDataFieldColor(string $color): ExcelSheet
    {
        $this->cellDataFieldColor = $color;

        return $this;
    }

    /**
     * @param bool $value
     * @return $this
     */
    public function disableInsertHiddenRow(bool $value = true): ExcelSheet
    {
        $this->insertHiddenRow = !$value;

        return $this;
    }

    /**
     * @param int $row
     * @return $this
     */
    public function setStartRow(int $row): ExcelSheet
    {
        $this->startRow = $row;

        return $this;
    }

    /**
     * @param int $column
     * @return $this
     */
    public function setStartColumn(int $column): ExcelSheet
    {
        $this->startColumn = $column;

        return $this;
    }

    /**
     * @param Closure $closure
     * @return $this
     */
    public function setHorizontalHiddenClosure(Closure $closure): ExcelSheet
    {
        $this->horizontalHiddenClosure = $closure;

        return $this;
    }

    /**
     * @param Closure $closure
     * @return $this
     */
    public function setVerticalHiddenClosure(Closure $closure): ExcelSheet
    {
        $this->verticalHiddenClosure = $closure;

        return $this;
    }

    /**
     * @param Closure $closure
     * @return $this
     */
    public function setInsertCellDataOnLeafCellClosure(Closure $closure): ExcelSheet
    {
        $this->insertCellDataOnLeafCellClosure = $closure;

        return $this;
    }

    /**
     * @param bool $value
     * @return $this
     */
    public function disableInsertHiddenColumn(bool $value = true): ExcelSheet
    {
        $this->insertHiddenColumn = !$value;

        return $this;
    }

    /**
     * @param Closure $closure
     * @return $this
     */
    public function setInsertExtraCellClosure(Closure $closure): ExcelSheet
    {
        $this->insertExtraCellClosure = $closure;

        return $this;
    }

    /**
     * @param bool $merge
     * @return $this
     */
    public function disableMergeCell(bool $merge = true): ExcelSheet
    {
        $this->mergeCell = !$merge;

        return $this;
    }

    /**
     * @param Closure $closure
     * @return $this
     */
    public function setCellDataVFieldClosure(Closure $closure): ExcelSheet
    {
        $this->cellDataVFieldClosure = $closure;

        return $this;
    }

    /**
     * @param bool $bold
     * @return $this
     */
    public function disableCellDataBold(bool $bold = true): ExcelSheet
    {
        $this->cellDataFieldBold = !$bold;

        return $this;
    }

    /**
     * @param int $value
     * @return $this
     */
    public function setCellDataFieldTextBreak(int $value): ExcelSheet
    {
        $this->cellDataFieldTextBreak = $value;

        return $this;
    }

    /**
     * @param int $type
     * @return $this
     */
    public function setCellDataFieldHorizontalType(int $type): ExcelSheet
    {
        $this->cellDataFieldHorizontalType = $type;

        return $this;
    }

    /**
     * @param int $type
     * @return $this
     */
    public function setCellDataFieldVerticalType(int $type): ExcelSheet
    {
        $this->cellDataFieldVerticalType = $type;

        return $this;
    }

    /**
     * @param Closure $closure
     * @return $this
     */
    public function setCustomResponseDataClosure(Closure $closure): ExcelSheet
    {
        $this->customResponseDataClosure = $closure;

        return $this;
    }

    /**
     * @return array
     */
    public function categoryDataToLuckySheetCellData(): array
    {
        if ($this->horizontal) {
            return $this->categoryDataToHorizontalLuckySheetCellData();
        }

        return $this->categoryDataToVerticalLuckySheetCellData();
    }

    /**
     * @return array
     */
    private function categoryDataToHorizontalLuckySheetCellData(): array
    {
        if (is_null($this->insertHiddenRow)) {
            $this->insertHiddenRow = true;
        }
        if (is_null($this->mergeCell)) {
            $this->mergeCell = true;
        }
        $this->horizontalAnalyse($this->data, $this->startRow - 1);
        $startColumn = $this->startColumn;
        $result = $this->generateHorizontalSheetCellData($this->data, $startColumn);

        return $this->formatResponseData($result);
    }

    /**
     * @param array $data
     * @param int $row
     * @return int
     */
    private function horizontalAnalyse(array &$data, int $row = -1): int
    {
        $row++;
        $column = 0;
        foreach ($data as &$v) {
            if (!empty($v[$this->childrenField])) {
                $childrenColumn = $this->horizontalAnalyse(
                    $v[$this->childrenField],
                    $row
                );
                $sheet = [
                    'row' => $row,
                    'column' => $childrenColumn
                ];
                $column += $childrenColumn;
            } else {
                $column++;
                $sheet = [
                    'row' => $row,
                    'column' => 1,
                ];
                $this->columnCount++;
            }
            $v['_sheet'] = $sheet;
        }

        if ($row + 1 > $this->rowCount) {
            $this->rowCount = $row + 1;
        }

        return $column;
    }

    /**
     * @param array $data
     * @param int $column
     * @return array
     */
    private function generateHorizontalSheetCellData(
        array $data,
        int   &$column = 0
    ): array
    {
        $cell = [];
        $merge = [];
        foreach ($data as $v) {
            $row = $v['_sheet']['row'];
            $cell[] = $this->generateCellData($row, $column, $v);
            if ($this->insertHiddenRow) {
                $cell = array_merge(
                    $cell,
                    $this->generateHiddenRow($v, $column)
                );
            }

            if (empty($v[$this->childrenField])) {
                if ($this->rowCount > $row + 1) {
                    $merge[$row . '_' . $column] = [
                        'r' => $row,
                        'c' => $column,
                        'rs' => $this->rowCount - $row,
                        'cs' => $v['_sheet']['column']
                    ];
                }
                if (is_callable($this->insertCellDataOnLeafCellClosure)) {
                    $cell = array_merge(
                        $cell,
                        call_user_func(
                            $this->insertCellDataOnLeafCellClosure,
                            $v,
                            $column,
                            $this->rowCount,
                            $this->columnCount
                        )
                    );
                }
                $column++;
            } else {
                if ($this->mergeCell) {
                    $merge[$row . '_' . $column] = [
                        'r' => $row,
                        'c' => $column,
                        'rs' => 1,
                        'cs' => $v['_sheet']['column']
                    ];
                } else {
                    for ($i = 1; $i < $v['_sheet']['column']; $i++) {
                        $cell[] = $this->generateCellData($row, $column + $i, $v);
                    }
                }
                $result = $this->generateHorizontalSheetCellData(
                    $v[$this->childrenField],
                    $column
                );

                $cell = array_merge($cell, $result['cell']);
                $merge = array_merge($merge, $result['merge']);
            }
        }

        return compact('cell', 'merge');
    }

    /**
     * @param $value
     * @param int $column
     * @return array[]
     */
    private function generateHiddenRow($value, int $column): array
    {
        if (is_callable($this->horizontalHiddenClosure)) {
            return call_user_func($this->horizontalHiddenClosure, $value, $column);
        }

        return [
            [
                'r' => $this->rowCount,
                'c' => $column,
                'v' => [
                    'v' => $value[$this->hiddenField],
                    'm' => $value[$this->hiddenField]
                ]
            ]
        ];
    }

    /**
     * @return array
     */
    private function categoryDataToVerticalLuckySheetCellData(): array
    {
        if (is_null($this->insertHiddenColumn)) {
            $this->insertHiddenColumn = true;
        }
        if (is_null($this->mergeCell)) {
            $this->mergeCell = false;
        }
        $this->verticalAnalyse($this->data, $this->startColumn - 1);
        $startRow = $this->startRow;
        $result = $this->generateVerticalSheetCellData($this->data, $startRow);

        return $this->formatResponseData($result);
    }

    /**
     * @param array $data
     * @param int $column
     * @return int
     */
    private function verticalAnalyse(array &$data, int $column = -1): int
    {
        $column++;
        $row = 0;
        foreach ($data as &$v) {
            if (!empty($v[$this->childrenField])) {
                $childrenRow = $this->verticalAnalyse(
                    $v[$this->childrenField],
                    $column
                );
                $sheet = [
                    'row' => $childrenRow,
                    'column' => $column
                ];
                $row += $childrenRow;
            } else {
                $row++;
                $sheet = [
                    'row' => 1,
                    'column' => $column,
                ];
                $this->rowCount++;
            }
            $v['_sheet'] = $sheet;
        }

        if ($column + 1 > $this->columnCount) {
            $this->columnCount = $column + 1;
        }

        return $row;
    }

    /**
     * @param array $data
     * @param int $row
     * @return array
     */
    private function generateVerticalSheetCellData(array $data, int &$row): array
    {
        $cell = [];
        $merge = [];
        foreach ($data as $v) {
            $column = $v['_sheet']['column'];
            $cell[] = $this->generateCellData($row, $column, $v);
            if ($this->insertHiddenColumn) {
                $cell = array_merge(
                    $cell,
                    $this->generateHiddenColumn($v, $row)
                );
            }

            if (empty($v[$this->childrenField])) {
                if ($this->columnCount > $column + 1) {
                    $merge[$row . '_' . $column] = [
                        'r' => $row,
                        'c' => $column,
                        'rs' => 1,
                        'cs' => $this->columnCount - $column
                    ];
                }
                if (is_callable($this->insertCellDataOnLeafCellClosure)) {
                    $cell = array_merge(
                        $cell,
                        call_user_func(
                            $this->insertCellDataOnLeafCellClosure,
                            $v,
                            $row,
                            $this->rowCount,
                            $this->columnCount
                        )
                    );
                }
                $row++;
            } else {
                if ($this->mergeCell) {
                    $merge[$row . '_' . $column] = [
                        'r' => $row,
                        'c' => $column,
                        'rs' => $v['_sheet']['row'],
                        'cs' => 1
                    ];
                } else {
                    for ($i = 1; $i < $v['_sheet']['row']; $i++) {
                        $cell[] = $this->generateCellData($row + $i, $column, $v);
                    }
                }
                $result = $this->generateVerticalSheetCellData(
                    $v[$this->childrenField],
                    $row
                );

                $cell = array_merge($cell, $result['cell']);
                $merge = array_merge($merge, $result['merge']);
            }
        }

        return compact('cell', 'merge');
    }

    /**
     * @param int $row
     * @param int $column
     * @param array $value
     * @return array
     */
    private function generateCellData(int $row, int $column, array $value): array
    {
        return [
            'r' => $row,
            'c' => $column,
            'v' => is_callable($this->cellDataVFieldClosure)
                ? call_user_func($this->cellDataVFieldClosure, $value, $row, $column)
                : [
                    'bl' => (int)$this->cellDataFieldBold,
                    'tb' => $this->cellDataFieldTextBreak,
                    'ht' => $this->cellDataFieldHorizontalType,
                    'vt' => $this->cellDataFieldVerticalType,
                    'fc' => $this->cellDataFieldColor,
                    'm' => is_callable($this->valueClosure)
                        ? call_user_func($this->valueClosure, $value, 'm')
                        : $value[$this->valueField],
                    'v' => is_callable($this->valueClosure)
                        ? call_user_func($this->valueClosure, $value, 'v')
                        : $value[$this->valueField]
                ]
        ];
    }

    /**
     * @param array $value
     * @param int $row
     * @return array[]
     */
    private function generateHiddenColumn(array $value, int $row): array
    {
        if (is_callable($this->verticalHiddenClosure)) {
            return call_user_func($this->verticalHiddenClosure, $value, $row);
        }

        return [
            [
                'r' => $row,
                'c' => $this->columnCount,
                'v' => [
                    'v' => $value[$this->hiddenField],
                    'm' => $value[$this->hiddenField],
                    'ct' => [
                        'fa' => 0,
                        't' => 'n'
                    ]
                ]
            ]
        ];
    }

    /**
     * @param array $data
     * @return array
     */
    private function formatResponseData(array $data): array
    {
        if ($this->insertHiddenRow) {
            $this->rowCount++;
        }

        if ($this->insertHiddenColumn) {
            $this->columnCount++;
        }

        $result = [
            'row' => $this->rowCount,
            'column' => $this->columnCount,
            'start_row' => $this->startRow,
            'start_column' => $this->startColumn,
            'cell' => $data['cell'],
            'merge' => $data['merge']
        ];

        if ($this->insertHiddenRow) {
            $result['rowhidden'] = [];
            $result['rowhidden'][$this->rowCount - 1] = 0;
        }

        if ($this->insertHiddenColumn) {
            $result['colhidden'] = [];
            $result['colhidden'][$this->columnCount - 1] = 0;
        }

        if (is_callable($this->insertExtraCellClosure)) {
            $res = call_user_func($this->insertExtraCellClosure, $result);
            if (!empty($res['cell'])) {
                $result['cell'] = array_merge($result['cell'], $res['cell']);
            }
            if (!empty($res['merge'])) {
                $result['merge'] = array_merge($result['merge'], $res['merge']);
            }
        }

        if (is_callable($this->customResponseDataClosure)) {
            return call_user_func($this->customResponseDataClosure, $result);
        }

        return $result;
    }
}
